import os
import re
import sys
import platform
import socket
from typing import Optional, Tuple
from datetime import datetime
import shutil

# 错误码描述映射
def format_exit_code(code: int) -> str:
    try:
        return f"{code} (0x{code & 0xFFFFFFFF:08X})"
    except Exception:
        return str(code)

def describe_wusa_code(code: int) -> str:
    mapping = {
        0: "成功",
        3010: "成功但需重启",
        2359302: "更新不适用或已被替代（典型：版本/架构/先决条件不满足，或已安装更高替代补丁）",
        5: "拒绝访问（权限不足）",
        2: "找不到文件或路径",
        1603: "安装程序内部错误",
        1638: "已安装相同或更高版本",
    }
    h = code & 0xFFFFFFFF
    hmap = {
        0x80070002: "找不到文件或路径",
        0x80070003: "路径无效",
        0x80070005: "拒绝访问（可能未以管理员运行或被策略阻止）",
        0x8007054F: "系统错误",
        0x800F081E: "更新不适用（功能/组件未启用或版本不匹配）",
        0x800F0922: "Servicing失败（可能系统保留分区空间不足或策略阻止）",
        0x80240017: "更新不适用",
        0x8024001B: "用户取消",
        0x80240007: "操作未完成/需重试",
    }
    if code in mapping:
        return mapping[code]
    if h in hmap:
        return hmap[h]
    if h == 0x00240006:
        return "更新不适用或已被替代"
    return "未知错误（查看补丁日志获取详细原因）"

def describe_exe_code(code: int) -> str:
    mapping = {
        0: "成功",
        3010: "成功但需重启",
        1641: "成功并已触发重启",
        1603: "安装程序内部错误/安装失败",
        1618: "另一安装正在运行（请稍后重试）",
        1638: "已安装相同或更高版本",
        1619: "无法打开安装包",
        1620: "无效的安装程序（或包损坏）",
        1639: "命令行参数无效",
        5: "拒绝访问（权限不足）",
        2: "找不到文件或路径",
    }
    h = code & 0xFFFFFFFF
    hmap = {
        0x80070002: "找不到文件或路径",
        0x80070003: "路径无效",
        0x80070005: "拒绝访问（可能未以管理员运行或被策略阻止）",
        0x80070643: "Windows Installer 安装失败（服务错误或包问题）",
        0x80240017: "更新不适用或先决条件不满足",
    }
    if code in mapping:
        return mapping[code]
    if h in hmap:
        return hmap[h]
    return "未知错误（查看补丁日志获取详细原因）"

def describe_dism_code(code: int) -> str:
    h = code & 0xFFFFFFFF
    mapping = {
        0x800F081E: "更新不适用（功能/组件未启用或镜像版本不匹配）",
        0x800F0823: "镜像版本不匹配（Version mismatch）",
        0x800F0831: "缺少清单/源（可能需要指定源或启用相关功能）",
        0x80070002: "找不到文件或路径",
        0x80070003: "路径无效",
        0x80070005: "拒绝访问（权限不足）",
        0x80240017: "更新不适用",
        123: "路径或参数格式不正确（常见：额外引号、非法字符或路径不存在）",
        87: "参数错误（常见：命令行选项不支持，如 /limitaccess）",
    }
    return mapping.get(h, "未知错误（查看DISM日志获取详细原因）")

# 系统相关函数
def get_os_tags() -> Tuple[str, str]:
    try:
        v = sys.getwindowsversion()
        ver = f"{v.major}.{v.minor}"
    except Exception:
        ver = platform.win32_ver()[1] or "10.0"
    if ver.startswith("6.3"):
        ver_tag = "windows8.1"
    elif ver.startswith("10.0"):
        ver_tag = "windows10.0"
    else:
        ver_tag = f"windows{ver}"
    arch = platform.machine().lower()
    arch_tag = "x64" if "64" in arch else "x86"
    return ver_tag, arch_tag

def is_legacy_windows() -> bool:
    """检测是否为低版本Windows系统（Windows 7或更早版本）"""
    try:
        v = sys.getwindowsversion()
        ver = f"{v.major}.{v.minor}"
    except Exception:
        ver = platform.win32_ver()[1] or "10.0"
    
    # Windows 7 = 6.1, Windows Vista = 6.0, Windows XP = 5.x
    return ver.startswith("5.") or ver.startswith("6.0") or ver.startswith("6.1")

def normalize_version_tag(tag: Optional[str]) -> Optional[str]:
    """统一版本标签格式"""
    if tag is None:
        return None
    t = tag.lower()
    if t in ("windows6.2", "windows8", "windows server 2012", "windowsserver2012", "server2012", "ws2012"):
        return "windows6.2"
    if t in ("windows6.3", "windows8.1", "windows server 2012 r2", "windowsserver2012r2", "server2012r2", "ws2012r2"):
        return "windows6.3"
    if t in ("windows10", "windows10.0", "windows server 2016", "windows server 2019", "server2016", "server2019"):
        return "windows10.0"
    return t

def check_patch_compatibility(filename: str) -> Tuple[bool, Optional[str]]:
    """检查补丁与当前系统的兼容性"""
    ver_tag, arch_tag = get_os_tags()
    name = filename.lower()
    want_ver = None
    
    # 基于文件名提取补丁目标版本
    m = re.search(r"windows(\d+(?:\.\d+)?)", name)
    if m:
        want_ver = f"windows{m.group(1)}"
    elif any(x in name for x in ["windows8.1", "win8.1", "server 2012 r2", "windowsserver2012r2", "ws2012r2"]):
        want_ver = "windows8.1"
    elif any(x in name for x in ["windows8", "win8", "server 2012", "windowsserver2012", "ws2012"]):
        want_ver = "windows8"
    elif any(x in name for x in ["windows10", "win10", "server 2016", "server 2019"]):
        want_ver = "windows10.0"

    norm_ver = normalize_version_tag(ver_tag)
    norm_want = normalize_version_tag(want_ver)

    # 架构判定
    want_arch = (
        "x64" if "x64" in name else (
            "x86" if "x86" in name else (
                "arm64" if "arm64" in name else (
                    "arm" if re.search(r"\brt\b", name) or "arm" in name else None
                )
            )
        )
    )

    if norm_want and norm_want != norm_ver:
        return False, f"补丁镜像版本为 {want_ver}，当前系统为 {ver_tag}"
    if want_arch and want_arch != arch_tag:
        return False, f"补丁架构为 {want_arch}，当前系统架构为 {arch_tag}"
    return True, None

# 文件处理函数
def read_log_text(path: str) -> Optional[str]:
    """读取日志文件内容，自动检测编码"""
    try:
        if not os.path.isfile(path):
            return None
        with open(path, "rb") as fb:
            data = fb.read()
        # BOM 检测优先
        encoding = None
        if data.startswith(b"\xff\xfe"):
            encoding = "utf-16le"
        elif data.startswith(b"\xfe\xff"):
            encoding = "utf-16be"
        elif data.startswith(b"\xef\xbb\xbf"):
            encoding = "utf-8-sig"
        tried = [e for e in [encoding, "utf-8", "utf-16", "gb18030", "mbcs", "latin-1"] if e]
        for enc in tried:
            try:
                text = data.decode(enc, errors="replace")
                return text
            except Exception:
                continue
        return None
    except Exception:
        return None

def extract_kb_id(filename: str) -> str:
    """从文件名中提取KB ID"""
    try:
        match = re.search(r'KB(\d+)', filename, re.IGNORECASE)
        if match:
            return match.group(0)
        return "未知KB"
    except Exception:
        return "未知KB"

def get_local_ip() -> str:
    """获取本机IP地址"""
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        return "127.0.0.1"

def extract_wusa_reason(patch_log: str, max_lines: int = 60) -> Optional[str]:
    """从WUSA日志中提取失败原因"""
    try:
        text = read_log_text(patch_log)
        if not text:
            return None
        lines = text.strip().split("\n")
        recent_lines = lines[-max_lines:] if len(lines) > max_lines else lines
        for line in reversed(recent_lines):
            if any(x in line.lower() for x in ["error", "failure", "fail", "不适用", "错误", "失败"]):
                return line.strip()
        return None
    except Exception:
        return None

def collect_failure_logs(base_dir: str, patch_name: str, patch_log: str, cmd_str: str = "", exit_code: Optional[int] = None, error_details: str = "") -> str:
    """收集失败日志"""
    try:
        # 创建失败日志目录
        fail_dir = os.path.join(base_dir, f"{datetime.now().strftime('%Y%m%d')}_失败日志")
        os.makedirs(fail_dir, exist_ok=True)
        
        # 生成失败日志文件名
        timestamp = datetime.now().strftime("%H%M%S")
        log_name = f"{timestamp}_{os.path.splitext(patch_name)[0]}_失败.txt"
        log_path = os.path.join(fail_dir, log_name)
        
        # 写入失败信息
        with open(log_path, "w", encoding="utf-8") as f:
            f.write(f"补丁名称: {patch_name}\n")
            f.write(f"失败时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
            if exit_code is not None:
                f.write(f"退出代码: {exit_code}\n")
            f.write(f"执行命令: {cmd_str}\n\n")
            
            # 写入详细错误信息（如果有）
            if error_details:
                f.write("===== 详细错误信息 =====\n")
                f.write(error_details)
                f.write("\n\n")
            
            # 复制原日志内容
            if os.path.isfile(patch_log):
                f.write("===== 原安装日志 =====\n")
                try:
                    log_content = read_log_text(patch_log)
                    if log_content:
                        f.write(log_content)
                except Exception as e:
                    f.write(f"读取原日志失败: {str(e)}\n")
        
        # 如果原日志存在，复制到失败目录
        if os.path.isfile(patch_log):
            shutil.copy2(patch_log, fail_dir)
        
        return log_name
    except Exception as e:
        # 记录收集日志失败的错误
        print(f"收集失败日志时出错: {str(e)}")
        return ""

# CLI模式专用工具函数
def format_exit_code_cli(code: int) -> str:
    return format_exit_code(code)

def describe_wusa_code_cli(code: int) -> str:
    return describe_wusa_code(code)

def describe_exe_code_cli(code: int) -> str:
    return describe_exe_code(code)

def describe_dism_code_cli(code: int) -> str:
    return describe_dism_code(code)

def collect_failure_logs_cli(base_dir: str, patch_name: str, patch_log: str, cmd_str: str = "", exit_code: Optional[int] = None, error_details: str = "") -> str:
    """CLI模式的失败日志收集"""
    try:
        # 创建失败日志目录
        fail_dir = os.path.join(base_dir, f"{datetime.now().strftime('%Y%m%d')}_失败日志")
        os.makedirs(fail_dir, exist_ok=True)
        
        # 生成失败日志文件名
        timestamp = datetime.now().strftime("%H%M%S")
        log_name = f"{timestamp}_{os.path.splitext(patch_name)[0]}_失败.txt"
        log_path = os.path.join(fail_dir, log_name)
        
        # 写入失败信息
        with open(log_path, "w", encoding="utf-8") as f:
            f.write(f"补丁名称: {patch_name}\n")
            f.write(f"失败时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
            if exit_code is not None:
                f.write(f"退出代码: {exit_code}\n")
            f.write(f"执行命令: {cmd_str}\n\n")
            
            # 写入详细错误信息（如果有）
            if error_details:
                f.write("===== 详细错误信息 =====\n")
                f.write(error_details)
                f.write("\n\n")
            
            # 复制原日志内容
            if os.path.isfile(patch_log):
                f.write("===== 原安装日志 =====\n")
                try:
                    log_content = read_log_text(patch_log)
                    if log_content:
                        f.write(log_content)
                except Exception as e:
                    f.write(f"读取原日志失败: {str(e)}\n")
        
        # 如果原日志存在，复制到失败目录
        if os.path.isfile(patch_log):
            shutil.copy2(patch_log, fail_dir)
        
        return log_name
    except Exception:
        return ""